/*
 * Copyright Red Hat Inc. and/or its affiliates and other contributors
 * as indicated by the authors tag. All rights reserved.
 *
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU General Public License version 2.
 * 
 * This particular file is subject to the "Classpath" exception as provided in the 
 * LICENSE file that accompanied this code.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT A
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License,
 * along with this distribution; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA  02110-1301, USA.
 */
package org.eclipse.ceylon.compiler.java.codegen.recovery;

import java.util.List;

import org.eclipse.ceylon.common.Backend;
import org.eclipse.ceylon.compiler.java.codegen.TypeVisitor;
import org.eclipse.ceylon.compiler.typechecker.analyzer.AnalysisError;
import org.eclipse.ceylon.compiler.typechecker.analyzer.UsageWarning;
import org.eclipse.ceylon.compiler.typechecker.tree.Message;
import org.eclipse.ceylon.compiler.typechecker.tree.Node;
import org.eclipse.ceylon.compiler.typechecker.tree.Tree;
import org.eclipse.ceylon.compiler.typechecker.tree.Visitor;
import org.eclipse.ceylon.model.loader.model.LazyClass;
import org.eclipse.ceylon.model.loader.model.LazyInterface;
import org.eclipse.ceylon.model.typechecker.model.Class;
import org.eclipse.ceylon.model.typechecker.model.Declaration;
import org.eclipse.ceylon.model.typechecker.model.Type;
import org.eclipse.ceylon.model.typechecker.model.Value;

/**
 * Visitor for declarations which constructs a {@link TransformationPlan} for 
 * transforming a {@link Tree.Declaration} based on the errors in the tree.
 * Unlike the {@link StatementErrorVisitor} and {@link ErrorVisitor}
 * this visitor does not fail fast.
 */
class DeclarationErrorVisitor extends Visitor {
    private static final int TYPE_DECLARATION_DOES_NOT_EXIST = 102;
    private static final int FORMAL_MEMBER_UNIMPLEMENTED_IN_CLASS_HIERARCHY = 300;
    private static final int MEMBER_HAS_WRONG_NUMBER_OF_PARAMETERS = 9100;
    private static final int TYPE_OF_PARAMETER_IS_DIFFERENT_TO_CORRESPONDING_PARAMETER = 9200;
    private static final int COULD_NOT_DETERMINE_PARAMETER_TYPE_SAME_AS_CORRESPONDING_PARAMETER = 9210;
    private static final int REFINED_MEMBER_WRONG_NUM_PL = 9300;
    private static final int MISSING_PL_FUNCTION_DECL = 1000;
    private static final int NO_CONSTRUCTORS = 1001;
    private static final int PL_AND_CONSTRUCTORS = 1002;
    private static final int FORWARD_DECL_NOT_IN_DECL_SECTION = 1450;
    
    private TransformationPlan plan;
    private final ExpressionErrorVisitor expressionVisitor;
    private boolean expectingError;
    private String errMessage;
    private Declaration model;
    
    DeclarationErrorVisitor(ExpressionErrorVisitor expressionVisitor) {
        this.expressionVisitor = expressionVisitor;
    }

    public final HasErrorException getFirstErrorMessage(Tree.Declaration target) {
        TransformationPlan plan = getRecoveryPlan(target);
        if (plan instanceof Generate) {
            return null;
        } else {
            return new HasErrorException(plan.getNode(), plan.getErrorMessage());
        }
    }
    
    public final TransformationPlan getRecoveryPlan(Tree.Declaration target) {
        model = target.getDeclarationModel();
        TransformationPlan oldPlan = plan;
        try {
            plan = Errors.GENERATE;
            target.visit(this);
            if (target.getDeclarationModel() != null) {
                target.getDeclarationModel().setDropped(plan instanceof Drop);
            }
            return plan;
        } finally {
            plan = oldPlan;
        }
    }
    
    /**
     * Install a new plan iff it's more drastic than the existing plan
     * @param plan
     */
    private void newplan(TransformationPlan plan) {
        if (plan.replaces(this.plan)) {
            this.plan = plan;
        }
    }
    
    @Override
    public final void visitAny(Node that) {
        planAccordingToErrors(that);
        super.visitAny(that);
    }
    
    /**
     * Update the plan according to the errors on the node
     */
    private void planAccordingToErrors(Node that) {
        List<Message> errors = that.getErrors();
        
        for(Message message : errors){
            if(isError(that, message)) {
                TransformationPlan plan;
                /*if (message.getCode() == MEMBER_HAS_WRONG_NUMBER_OF_PARAMETERS
                        && model.isActual()
                        && model.isClassMember()) {
                    plan = new ThrowerMethod(that, message);
                } else if (message.getCode() == TYPE_OF_PARAMETER_IS_DIFFERENT_TO_CORRESPONDING_PARAMETER
                        && model.isActual()
                        && model.isClassMember()) {
                    plan = new ThrowerMethod(that, message);
                } else if (message.getCode() == COULD_NOT_DETERMINE_PARAMETER_TYPE_SAME_AS_CORRESPONDING_PARAMETER
                        && model.isActual()
                        && model.isClassMember()) {
                    plan = new ThrowerMethod(that, message);
                } else if ((message.getCode() == REFINED_MEMBER_WRONG_NUM_PL
                                || message.getCode() == MISSING_PL_FUNCTION_DECL)
                        && model.isActual()
                        && model.isClassMember()) {
                    plan = new ThrowerMethod(that, message);
                } else*/ if (message.getCode() == FORMAL_MEMBER_UNIMPLEMENTED_IN_CLASS_HIERARCHY
                        && (model instanceof Class 
                            || (model instanceof Value
                                    && ((Value)model).getTypeDeclaration().isAnonymous()))) {
                    plan = new ThrowerMethod(that, message);
                } 
                else if ((message.getCode() == PL_AND_CONSTRUCTORS || message.getCode() == NO_CONSTRUCTORS)
                        && (model instanceof Class 
                            || (model instanceof Value
                                    && ((Value)model).getTypeDeclaration().isAnonymous()))) {
                    if (message.getCode() == NO_CONSTRUCTORS) {
                        plan = new PrivateConstructorOnly(that, message);
                    } else {
                        plan = new ThrowerCatchallConstructor(that, message);
                    }
                }
                else if (message.getCode() == FORWARD_DECL_NOT_IN_DECL_SECTION) {
                    plan = Errors.GENERATE; 
                }
                else {
                    plan = new Drop(that, message);
                } 
                newplan(plan);
            }
        }
    }
    
    /** Is the given message on the given node considered an error */
    private boolean isError(Node that, Message message) {
        if (errMessage != null
                && message.getMessage().equals(errMessage)) {
            return false;
        } else {
            return !(message instanceof UsageWarning);
        }
    }
    
    @Override
    public void visit(Tree.Annotation that) {
        // don't go there
    }
    
    @Override
    public void visit(Tree.Body that) {
        // don't go there: we don't really care about block errors
    }
    
    @Override
    public void visit(Tree.SpecifierOrInitializerExpression that) {
        // don't go there: we don't really care about expression errors
    }
    
    @Override
    public void visit(Tree.Type that) {
        HasErrorException error = expressionVisitor.getFirstErrorMessage(that);
        if (error != null && isError(that, error.getErrorMessage())) {
            newplan(new Drop(error.getNode(), error.getErrorMessage()));
            return;
        }
        // type inference is used but the type of 
        // the inferred expression is unknown due to other errors
        if (containsUnknowns(that.getTypeModel())) {
            newplan(new Drop(that, new AnalysisError(that, "unknown type", Backend.Java)));
        }
    }
    
    boolean containsUnknowns(Type t) {
        class UnknownVisitor extends TypeVisitor {
            boolean staticJavaMember = false;
            boolean unknowns = false;
            @Override
            public void visitUnknown() {
                unknowns = true;
            }
            @Override
            public void visitTypeArguments(Type typeConstructor, List<Type> typeArguments) {
                if (!staticJavaMember) {
                    super.visitTypeArguments(typeConstructor, typeArguments);
                }
            }
            @Override
            public void visitQualifyingType(Type qualified, Type qualifying) {
                boolean unknownOk = this.staticJavaMember;
                this.staticJavaMember = isJava(qualifying) && qualified.getDeclaration().isStatic();
                super.visitQualifyingType(qualified, qualifying);
                this.staticJavaMember = unknownOk;
            }
            private boolean isJava(Type type) {
                boolean isjava;
                if (type.getDeclaration() instanceof LazyClass) {
                    isjava = !((LazyClass)type.getDeclaration()).isCeylon(); 
                } else if (type.getDeclaration() instanceof LazyInterface) {
                    isjava = !((LazyInterface)type.getDeclaration()).isCeylon(); 
                } else {
                    isjava = false;
                }
                return isjava;
            }
        };
        UnknownVisitor uv = new UnknownVisitor();
        uv.visitType(t);
        return uv.unknowns;
    }
    
    @Override
    public void visit(Tree.InitializerParameter that) {
        planAccordingToErrors(that);
        // but don't visit children
    }
    
    public void visit(Tree.StatementOrArgument that) {
        boolean b = this.expectingError;
        initExpectingError(that.getCompilerAnnotations());
        super.visit(that);
        expectingError = b;
    }
    
    public void visit(Tree.ParameterDeclaration that) {
        boolean b = this.expectingError;
        initExpectingError(that.getTypedDeclaration().getCompilerAnnotations());
        super.visit(that);
        expectingError = b;
    }
    
    public void visit(Tree.CompilationUnit that) {
        boolean b = this.expectingError;
        initExpectingError(that.getCompilerAnnotations());
        super.visit(that);
        expectingError = b;
    }
    
    protected void initExpectingError(List<Tree.CompilerAnnotation> annotations) {
        for (Tree.CompilerAnnotation c: annotations) {
            if (c.getIdentifier().getText().equals("error")) {
                expectingError = true;
                Tree.StringLiteral sl = c.getStringLiteral();
                if (sl!=null) {
                    errMessage = sl.getText();
                }
            }
        }
    }
    
    public void visit(Tree.ExtendedType that) {
        that.getType().visit(this);
        // Don't visit the invocation expression unless our superclass is a 
        // Java class with a private ctor -- that could cause a javac 
        // error if we tried to generate a ctor for this class.
        if (that.getType().getDeclarationModel() instanceof LazyClass
                && !((LazyClass)that.getType().getDeclarationModel()).isCeylon()) {
            boolean hasPrivateCtor = false;
            List<Declaration> overloads = ((LazyClass)that.getType().getDeclarationModel()).getOverloads();
            if (overloads != null) {
                for (Declaration ctor : overloads) {
                    if (!ctor.isShared()) {
                        hasPrivateCtor = true;
                        break;
                    }
                }
            }
            if (hasPrivateCtor && that.getInvocationExpression() != null) {
                that.getInvocationExpression().visit(this);
            }
        }
    }
}